/*
 * Copyright 1998-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package de.blacksheep.jmoteextract.filesystem;


import de.blacksheep.jmoteextract.filesystem.CifsFileSystemView.FileSystemRoot;
import java.net.MalformedURLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;

import java.io.File;
import java.io.IOException;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.filechooser.FileSystemView;
import javax.swing.filechooser.FileView;
import jcifs.smb.NtlmPasswordAuthentication;
import jcifs.smb.SmbException;


import jcifs.smb.SmbFile;


/**
 * FileSystemView is JFileChooser's gateway to the
 * file system. Since the JDK1.1 File API doesn't allow
 * access to such information as root partitions, file type
 * information, or hidden file bits, this class is designed
 * to intuit as much OS-specific file system information as
 * possible.
 *
 * <p>
 *
 * Java Licensees may want to provide a different implementation of
 * FileSystemView to better handle a given operating system.
 *
 * @author Jeff Dinkins
 */

// PENDING(jeff) - need to provide a specification for
// how Mac/OS2/BeOS/etc file systems can modify FileSystemView
// to handle their particular type of file system.

public class CifsFileSystemView extends FileSystemView {

    private static CifsFileSystemView cifsFileSystemView = null;
    private static boolean useSystemExtensionsHiding = false;
    
    private static String server;
    private static NtlmPasswordAuthentication auth;
    
    private static String currentDirectory;

    public static CifsFileSystemView getFileSystemView() {
        useSystemExtensionsHiding = UIManager.getDefaults().getBoolean("FileChooser.useSystemExtensionHiding");
        UIManager.addPropertyChangeListener(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
               if (e.getPropertyName().equals("lookAndFeel")) {
                   useSystemExtensionsHiding = UIManager.getDefaults().getBoolean("FileChooser.useSystemExtensionHiding");
               }
            }
        });
        
        if (cifsFileSystemView == null) {
            cifsFileSystemView = new CifsFileSystemView();
        }
        
        return cifsFileSystemView;
    }
    
    public void setServer(String server) {
        CifsFileSystemView.server = server;
    }
    
    public void setAuth(NtlmPasswordAuthentication auth) {
        CifsFileSystemView.auth = auth;
    }
    
    public static void setCurrentDirectory(String currentDirectory) {
        CifsFileSystemView.currentDirectory = currentDirectory;
    }
    
    public static boolean smbExists(File f) {
        boolean exists = false;
        SmbFile sf = fileToSmb(f);
        try {
            exists = sf.exists();
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        return exists;
    }
    
    public static SmbFile fileToSmb(File f) {
        if (f == null || !f.isAbsolute()) {
            return null;
        }
        
        SmbFile sf = null;
        try {
            sf = new SmbFile(uncToSmbUrl(f.getAbsolutePath()), auth);
        } catch (MalformedURLException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        return sf;
    }
    
    public static String uncToSmbUrl(String url) {
        url = url.replaceAll("\\\\", "/");
        url = "smb:" + url;
        
        if (!url.matches("/$")) {
            url = url.concat("/");
        }
        
        return url;
    }
    
    public static File smbToFile(SmbFile sf) {
        if (sf == null) {
            return null;
        }
        
        return new File(sf.getUncPath());
    }

    /**
     * Determines if the given file is a root in the navigatable tree(s).
     * Examples: Windows 98 has one root, the Desktop folder. DOS has one root
     * per drive letter, <code>C:\</code>, <code>D:\</code>, etc. Unix has one root,
     * the <code>"/"</code> directory.
     *
     * The default implementation gets information from the <code>ShellFolder</code> class.
     *
     * @param f a <code>File</code> object representing a directory
     * @return <code>true</code> if <code>f</code> is a root in the navigatable tree.
     * @see #isFileSystemRoot
     */
    @Override
    public boolean isRoot(File f) {       
        SmbFile sf = fileToSmb(f);
        
        if (sf == null) {
            return false;
        }

        File[] roots = getRoots();
        for (int i = 0; i < roots.length; i++) {            
            if (roots[i].getAbsolutePath().equals(f.getAbsolutePath())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if the file (directory) can be visited.
     * Returns false if the directory cannot be traversed.
     *
     * @param f the <code>File</code>
     * @return <code>true</code> if the file/directory can be traversed, otherwise <code>false</code>
     * @see JFileChooser#isTraversable
     * @see FileView#isTraversable
     * @since 1.4
     */
    @Override
    public Boolean isTraversable(File f) {        
        SmbFile sf = fileToSmb(f);
        
        if (sf == null) {
            return false;
        }
        
        boolean isDirectory = false;
        try {
            isDirectory = sf.isDirectory();
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        return isDirectory;
    }

    /**
     * Name of a file, directory, or folder as it would be displayed in
     * a system file browser. Example from Windows: the "M:\" directory
     * displays as "CD-ROM (M:)"
     *
     * The default implementation gets information from the ShellFolder class.
     *
     * @param f a <code>File</code> object
     * @return the file name as it would be displayed by a native file chooser
     * @see JFileChooser#getName
     * @since 1.4
     */
    @Override
    public String getSystemDisplayName(File f) {        
        SmbFile sf = fileToSmb(f);
        
        String name = null;
        if (sf != null) {
            name = sf.getName();
            if (name.endsWith("/")) {
                name = name.substring(0, name.length() - 1);
            }
        }
        return name;
    }

    /**
     * Type description for a file, directory, or folder as it would be displayed in
     * a system file browser. Example from Windows: the "Desktop" folder
     * is desribed as "Desktop".
     *
     * Override for platforms with native ShellFolder implementations.
     *
     * @param f a <code>File</code> object
     * @return the file type description as it would be displayed by a native file chooser
     * or null if no native information is available.
     * @see JFileChooser#getTypeDescription
     * @since 1.4
     */
    @Override
    public String getSystemTypeDescription(File f) {        
        return null;
    }

    /**
     * Icon for a file, directory, or folder as it would be displayed in
     * a system file browser. Example from Windows: the "M:\" directory
     * displays a CD-ROM icon.
     *
     * The default implementation gets information from the ShellFolder class.
     *
     * @param f a <code>File</code> object
     * @return an icon as it would be displayed by a native file chooser
     * @see JFileChooser#getIcon
     * @since 1.4
     */
    @Override
    public Icon getSystemIcon(File f) {        
        SmbFile sf = fileToSmb(f);
        
        if (sf != null) {
            try {
                return UIManager.getIcon(sf.isDirectory() ? "FileView.directoryIcon" : "FileView.fileIcon");
            } catch (SmbException ex) {
                Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
            }
        } 
        return null;
    }

    /**
     * On Windows, a file can appear in multiple folders, other than its
     * parent directory in the filesystem. Folder could for example be the
     * "Desktop" folder which is not the same as file.getParentFile().
     *
     * @param folder a <code>File</code> object repesenting a directory or special folder
     * @param file a <code>File</code> object
     * @return <code>true</code> if <code>folder</code> is a directory or special folder and contains <code>file</code>.
     * @since 1.4
     */
    @Override
    public boolean isParent(File folder, File file) {        
        /*if (folder == null || file == null) {
            return false;
        } else if (folder instanceof ShellFolder) {
                File parent = file.getParentFile();
                if (parent != null && parent.equals(folder)) {
                    return true;
                }
            File[] children = getFiles(folder, false);
            for (int i = 0; i < children.length; i++) {
                if (file.equals(children[i])) {
                    return true;
                }
            }
            return false;
        } else {
            return folder.equals(file.getParentFile());
        }*/
        return false;
    }

    /**
     *
     * @param parent a <code>File</code> object repesenting a directory or special folder
     * @param fileName a name of a file or folder which exists in <code>parent</code>
     * @return a File object. This is normally constructed with <code>new
     * File(parent, fileName)</code> except when parent and child are both
     * special folders, in which case the <code>File</code> is a wrapper containing
     * a <code>ShellFolder</code> object.
     * @since 1.4
     */
    @Override
    public File getChild(File parent, String fileName) {        
        /*if (parent instanceof ShellFolder) {
            File[] children = getFiles(parent, false);
            for (int i = 0; i < children.length; i++) {
                if (children[i].getName().equals(fileName)) {
                    return children[i];
                }
            }
        }
        return createFileObject(parent, fileName);*/
        return null;
    }


    /**
     * Checks if <code>f</code> represents a real directory or file as opposed to a
     * special folder such as <code>"Desktop"</code>. Used by UI classes to decide if
     * a folder is selectable when doing directory choosing.
     *
     * @param f a <code>File</code> object
     * @return <code>true</code> if <code>f</code> is a real file or directory.
     * @since 1.4
     */
    @Override
    public boolean isFileSystem(File f) {        
        /*if (f instanceof ShellFolder) {
            ShellFolder sf = (ShellFolder)f;
            // Shortcuts to directories are treated as not being file system objects,
            // so that they are never returned by JFileChooser.
            return sf.isFileSystem() && !(sf.isLink() && sf.isDirectory());
        } else {
            return true;
        }*/
        return true;
    }

    /**
     * Creates a new folder with a default folder name.
     */
    public File createNewFolder(File containingDir) throws IOException {   
        SmbFile sf = fileToSmb(containingDir);
        
        if (sf == null || !sf.isDirectory()) {
            throw new IOException("Containing directory is null or no directory!");
        }
        
        String newFolderString = UIManager.getString("FileChooser.other.newFolder");

        File newFolder = null;
        // Using NT's default folder name
        newFolder = createFileObject(containingDir, newFolderString);
        
        SmbFile nf = fileToSmb(newFolder);
        
        if (nf == null) {
            throw new IOException("Error converting!");
        }

        if(nf.exists()) {
            throw new IOException("Directory already exists:" + nf.getPath());
        } else {
            nf.mkdirs();
        }

        return newFolder;
    }

    /**
     * Returns whether a file is hidden or not.
     */
    @Override
    public boolean isHiddenFile(File f) {        
        SmbFile sf = fileToSmb(f);
        
        if (sf == null) {
            return true;
        }
        
        boolean isHidden = true;
        try {
            isHidden = (sf.isHidden() || sf.getUncPath().endsWith("$")) ? true : false;
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        return isHidden;
    }


    /**
     * Is dir the root of a tree in the file system, such as a drive
     * or partition. Example: Returns true for "C:\" on Windows 98.
     *
     * @param dir a <code>File</code> object representing a directory
     * @return <code>true</code> if <code>f</code> is a root of a filesystem
     * @see #isRoot
     * @since 1.4
     */
    @Override
    public boolean isFileSystemRoot(File dir) {        
        SmbFile sf = fileToSmb(dir);
        
        if (sf == null) {
            return false;
        }
        
        if (sf.getParent().equals(String.format("smb://", server))) {
            return true;
        }
        
        return false;
    }

    /**
     * Used by UI classes to decide whether to display a special icon
     * for drives or partitions, e.g. a "hard disk" icon.
     *
     * The default implementation has no way of knowing, so always returns false.
     *
     * @param dir a directory
     * @return <code>false</code> always
     * @since 1.4
     */
    @Override
    public boolean isDrive(File dir) {
        return false;
    }

    /**
     * Used by UI classes to decide whether to display a special icon
     * for a floppy disk. Implies isDrive(dir).
     *
     * The default implementation has no way of knowing, so always returns false.
     *
     * @param dir a directory
     * @return <code>false</code> always
     * @since 1.4
     */
    @Override
    public boolean isFloppyDrive(File dir) {
        return false;
    }

    /**
     * Used by UI classes to decide whether to display a special icon
     * for a computer node, e.g. "My Computer" or a network server.
     *
     * The default implementation has no way of knowing, so always returns false.
     *
     * @param dir a directory
     * @return <code>false</code> always
     * @since 1.4
     */
    @Override
    public boolean isComputerNode(File dir) {
        return false;
    }


    /**
     * Returns all root partitions on this system. For example, on
     * Windows, this would be the "Desktop" folder, while on DOS this
     * would be the A: through Z: drives.
     */
    @Override
    public File[] getRoots() {
        // Have to call this, to avoid freeze of getFiles later
        try {
            SmbFile sf = new SmbFile(String.format("smb://%s/", server), auth);
            sf.listFiles();
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        } catch (MalformedURLException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        // Only root directory should be the server's index of shares
        File[] fRoots2 = new File[] {
            new File(String.format("\\\\%s", server))
        };
        
        return fRoots2;
    }

    @Override
    public File getHomeDirectory() {        
        return createFileObject(String.format("smb://%s/", server));
    }

    /**
     * Return the user's default starting directory for the file chooser.
     *
     * @return a <code>File</code> object representing the default
     *         starting folder
     * @since 1.4
     */
    @Override
    public File getDefaultDirectory() {        
        File f = createFileObject(String.format("smb://%s/", server));
        if (isFileSystemRoot(f)) {
            f = createFileSystemRoot(f);
        }
        return f;
    }

    /**
     * Returns a File object constructed in dir from the given filename.
     */
    @Override
    public File createFileObject(File dir, String filename) {          
        if(dir == null) {
            return new File(filename);
        } else {
            return new File(dir, filename);
        }
    }

    /**
     * Returns a File object constructed from the given path string.
     */
    @Override
    public File createFileObject(String path) {         
        if (!(path.contains("\\") || path.contains("/"))) {
            path = currentDirectory.concat("\\").concat(path);
            path = uncToSmbUrl(path);
        }
        
        SmbFile sf = null;
        try {
            sf = new SmbFile(path, auth);
        } catch (MalformedURLException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        if (sf == null) {
            return null;
        }
        
        File f = new File(sf.getUncPath());
        if (isFileSystemRoot(f)) {
            f = createFileSystemRoot(f);
        }
        return f;
    }


    /**
     * Gets the list of shown (i.e. not hidden) files.
     */
    @Override
    public File[] getFiles(File dir, boolean useFileHiding) {        
        Collection<File> files = new ArrayList<File>();
        
        SmbFile sf = fileToSmb(dir);

        // add all files in dir
        SmbFile[] names = null;
        try {
            names = sf.listFiles();
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }

        SmbFile f;
        int nameCount = (names == null) ? 0 : names.length;
        for (int i = 0; i < nameCount; i++) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
            f = names[i];
            if (isFileSystemRoot(smbToFile(f))) {
                f = fileToSmb(createFileSystemRoot(smbToFile(f)));
            }
            if (!useFileHiding || !isHiddenFile(smbToFile(f))) {
                files.add(smbToFile(f));
            }
        }

        return (File[]) files.toArray(new File[files.size()]);
    }

    /**
     * Returns the parent directory of <code>dir</code>.
     * @param dir the <code>File</code> being queried
     * @return the parent directory of <code>dir</code>, or
     *   <code>null</code> if <code>dir</code> is <code>null</code>
     */
    @Override
    public File getParentDirectory(File dir) {        
        SmbFile sf = fileToSmb(dir);
        
        try {
            if (sf != null && sf.exists()) {
                String parent = sf.getParent();
                sf = new SmbFile(parent, auth);
                
                return smbToFile(sf);
            }
        } catch (MalformedURLException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SmbException ex) {
            Logger.getLogger(CifsFileSystemView.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        return null;
    }

    /**
     * Creates a new <code>File</code> object for <code>f</code> with correct
     * behavior for a file system root directory.
     *
     * @param f a <code>File</code> object representing a file system root
     *          directory, for example "/" on Unix or "C:\" on Windows.
     * @return a new <code>File</code> object
     * @since 1.4
     */
    @Override
    protected File createFileSystemRoot(File f) {        
        return new FileSystemRoot(f);
    }
    
    static class FileSystemRoot extends File {
        public FileSystemRoot(File f) {
            super(f,"");
        }

        public FileSystemRoot(String s) {
            super(s);
        }

        @Override
        public boolean isDirectory() {
            return true;
        }

        @Override
        public String getName() {
            return getPath();
        }
    }
}